#include <iomanip>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <cstdlib>

#include <GL/glew.h>
#include <GL/glut.h>

#include <Cg/cg.h>
#include <Cg/cgGL.h>

using namespace std;

GLint size;
GLfloat  w;

// The texture to hold data on the GPU.
GLuint d_texture;

// Handle to frame buffer object
GLuint fb;

// Arrays on the CPU to hold the data and result of GPU calculation.
GLfloat * h_data;
GLfloat * h_result;

// Some CG variables.
CGcontext cgContext;
CGprogram cgProgram;
CGprofile cgFragmentProfile;
CGparameter d_data = 0; // texture handle
CGparameter d_w = 0;
CGparameter d_dataSize = 0;
const GLenum texTarget = GL_TEXTURE_RECTANGLE_ARB;
const GLenum texFormat = GL_RGBA_FLOAT32_ATI;

void createCPUdata(int argc, char ** argv);
void initGLUT(int argc, char ** argv);
void initGLEW();
void set1to1Mapping();
void initFBO();
void initGPUTextures();
void copyTexturesToGPU();
void initCgProg();
void render();
void readBacktoCPU();
void printResults();
void printResultstoFile(std::string fileName);


int main(int argc, char ** argv)
{
    if (argc != 3)
    {
        cout << "USAGE: ./test5 [size] [w value]" << endl;
        exit(1);
    }

    // --------------------------------------------------------------------
    size = atoi(argv[1]);
    w = atof(argv[2]);
    // --------------------------------------------------------------------

    createCPUdata(argc, argv);
    initGLUT(argc, argv);
    initGLEW();
    set1to1Mapping();
    initFBO();
    initGPUTextures();
    copyTexturesToGPU();
    initCgProg();
    render();
    readBacktoCPU();
//    printResults();
    printResultstoFile("output_t5");
    // In gnuplot to plot the output:
    // splot "./output_t5.r" using 1:2:3 with lines
    // splot "./output_t5.i" using 1:2:3 with lines

    free(h_data);
    free(h_result);
    glDeleteTextures (1, &d_texture);
    cgDestroyContext(cgContext);
    return 0;
}


void createCPUdata(int argc, char **argv)
{
    enum { R, G, B, A };
    size_t bytes = size * size * 4 * sizeof(GLfloat);
    h_data = (GLfloat*) malloc(bytes);
    h_result = (GLfloat*) malloc(bytes);
    for (int i=0;i<size; i++)
    {
        for (int j=0;j<size;j++)
        {
            const int idx = 4 * ((size * i) + j);
            h_data[idx + R] = 0.1f;
            h_data[idx + G] = 1.1f;
            h_data[idx + B] = 2.1f;
            h_data[idx + A] = 3.1f;

            h_result[idx + R] = 0.0f;
            h_result[idx + G] = 0.0f;
            h_result[idx + B] = 0.0f;
            h_result[idx + A] = 0.0f;
        }
    }
}

void initGLUT(int argc, char** argv)
{
    glutInit (&argc, argv);
    glutCreateWindow("");
}

void initGLEW()
{
    glewInit();
}

void set1to1Mapping()
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, size, 0.0, size);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glViewport(0, 0, size, size);
}


void initFBO()
{
    glGenFramebuffersEXT(1, &fb);
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
}


void initGPUTextures()
{
    glGenTextures (1, &d_texture);
    glBindTexture(texTarget, d_texture);
    glTexParameteri(texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glTexImage2D(texTarget, 0, texFormat, size, size, 0, GL_RGBA, GL_FLOAT, 0);
}

void copyTexturesToGPU()
{
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
            texTarget, d_texture, 0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glRasterPos2i(0, 0);
    glDrawPixels(size, size, GL_RGBA, GL_FLOAT, h_data);
}


void initCgProg()
{
    const char * programSource = "kernels/wConvFunc.frag";
    const char * programEntry  = "genConvFunc";

    cgContext = cgCreateContext();
    if (!cgContext)
    {
        cerr << "ERROR: CG Context creation failure" << endl;
        exit(1);
    }
    cgFragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
    if (cgFragmentProfile == CG_PROFILE_UNKNOWN)
    {
        cerr << "Invalid Fragment Program profile" << endl;
        exit(1);
    }
    cgGLSetOptimalOptions(cgFragmentProfile);
    cgProgram  = cgCreateProgramFromFile(
            cgContext,         // the context where we want the shader added
            CG_SOURCE,         // array of bytes containing the cg source
            programSource,     // the source file (char *)
            cgFragmentProfile, // the profile
            programEntry,      // the entry function
            0
    );
    if (!cgProgram)
    {
        cerr << cgGetErrorString(cgGetError()) << endl;
        exit(1);
    }
    cgGLLoadProgram(cgProgram);

    d_data = cgGetNamedParameter(cgProgram, "d_data");
    cgGLSetTextureParameter(d_data, d_texture);
    cgGLEnableTextureParameter(d_data);

    d_dataSize = cgGetNamedParameter(cgProgram, "d_size");
    cgSetParameter1i(d_dataSize, size);

    d_w = cgGetNamedParameter(cgProgram, "d_w");
    cgSetParameter1f(d_w, w);
}

void render()
{
    cgGLBindProgram(cgProgram);
    cgGLSetTextureParameter(d_data, d_texture);
    cgGLEnableTextureParameter(d_data);
    cgGLEnableProfile(cgFragmentProfile);
    glPolygonMode(GL_FRONT, GL_FILL);
    glBegin(GL_QUADS);
    {
        glTexCoord2f(0.0, 0.0);
        glVertex2f(0.0, 0.0);
        glTexCoord2f(size, 0.0);
        glVertex2f(size, 0.0);
        glTexCoord2f(size, size);
        glVertex2f(size, size);
        glTexCoord2f(0.0, size);
        glVertex2f(0.0, size);
    }
    glEnd();
    cgGLDisableProfile(cgFragmentProfile);
}

void readBacktoCPU()
{
    glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glReadPixels(0, 0, size, size, GL_RGBA, GL_FLOAT, h_result);
}

void printResults()
{
    enum { R, G, B, A};
    cout << "* Data before GPU:" << endl;
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++) {
            const int idx = 4 * ((size * i) + j);
            cout << h_data[idx + R] << " ";
            cout << h_data[idx + G] << " ";
            cout << h_data[idx + B] << " ";
            cout << h_data[idx + A] << endl;
        }
    }
    cout << endl;

    cout << "* Data after GPU: " << endl;
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++)
        {
            const int idx = 4 * ((size * i) + j);
            cout.precision(4);
            cout << setw(10) << left << h_result[idx + R];
            cout << setw(10) << left << h_result[idx + G];
            cout << setw(10) << left << h_result[idx + G];
            cout << setw(10) << left << h_result[idx + A];
            cout << endl;
        }
        cout << endl;
    }
}


void printResultstoFile(std::string fileName)
{
    enum { R, G, B, A };
    ofstream outRe;
    ofstream outIm;
    string sOutfile_Re = fileName + ".r";
    string sOutfile_Im = fileName + ".i";
    const char * cOutfile_Re = sOutfile_Re.c_str();
    const char * cOutfile_Im = sOutfile_Im.c_str();
    outRe.open(cOutfile_Re);
    outIm.open(cOutfile_Im);
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++)
        {
            const int idx = 4 * ((size * i) + j);

            outRe << h_result[idx + R] << "\t";
            outRe << h_result[idx + G] << "\t";
            outRe << h_result[idx + B] << endl;

            outIm << h_result[idx + R] << "\t";
            outIm << h_result[idx + G] << "\t";
            outIm << h_result[idx + A] << endl;

        }
        outRe << endl;
        outIm << endl;
    }
    outRe.close();
    outIm.close();
}

